/*
Copyright 2011 Google Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.google.rsm.core;

import com.google.common.base.Preconditions;

import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A generic State machine. This class is not thread safe. It is up to
 * the callers to ensure that only one event is active in the state
 * machine at a time. This class is not intended to be used directly,
 * rather use the {@link StateCreator} class for creating an instance
 * of the GenericStateMachine.
 */
public class GenericStateMachine {
  private static final Logger logger = Logger.getLogger(GenericStateMachine.class.getName());

  /**
   * The array of States. If a state 'S' has an index i in the {@code
   * stateIndex} then states[i] = S.
   */
  private final String[] states;

  /**
   * The array of StateMachines. If a state 'S' is a recursive state
   * (i.e a state which is itself a state machine), and if 'S' has
   * index 'i' in the {@code stateIndex} then stateMachines[i] = the
   * state machine for state 'i', otherwise null.
   */
  private final State[] stateMachines;

  /**
   * The map from event types to indexes, supports the {@code
   * getEventIndex()} operation.
   */
  private final HashMap<EventType, Integer> eventIndex;

  /**
'   * The transition table. The rows are the States and the columns are
   * the Events. A value of -1 at a given row col means that a transition is
   * undefined for the particular combintaion of state and event.
   *
   * State          Event --->
   *   |
   *   V
   * ----------------------------------
   *      s1   | e1 | e2 | ....
   */
  private final int[][] transitionTable;

  /**
   * The event hooks to call on specified combinations of state and
   * received events.
   */
  private final List<EventCallback>[][] hooks;

  /**
   * The hooks to call when the state machine transitions to a
   * particular state. Note that the callback is called <em> only
   * </em> when the state machine transitions into the state,so if the
   * start state has a hook attached it wont be called when the state
   * mahcine starts.
   */
  private final List<StateCallback>[] stateHooks;

  private final int numStates;
  private final int numEvents;

  /**
   * The currentState, states[currentState] is the name of the currentState.
   */
  private int currentState;

  public final String name;

  public GenericStateMachine(HashMap<String, Integer> stateIndex,
                             HashMap<EventType, Integer> eventIndex,
                             int[][] transitionTable,
                             List<EventCallback>[][] hooks,
                             List<StateCallback>[] stateHooks,
                             String[] states,
                             State[] stateMachines,
                             String name,
                             int startState) {
    Preconditions.checkNotNull(stateIndex);
    Preconditions.checkNotNull(eventIndex);
    Preconditions.checkNotNull(transitionTable);
    Preconditions.checkNotNull(hooks);
    Preconditions.checkNotNull(stateHooks);
    Preconditions.checkNotNull(states);
    Preconditions.checkNotNull(stateMachines);

    numStates = stateIndex.size();
    numEvents = eventIndex.size();

    Preconditions.checkArgument((numStates > 0),
                                "Cannot run a StateMachine with no states");

    Preconditions.checkArgument((numEvents > 0),
                                "Cannot run a StateMachine with no events");

    Preconditions.checkArgument((transitionTable.length == numStates),
                                "Wrong rowsize for transitionTable. Expected %s"
                                + " got %s",
                                numStates,
                                transitionTable.length);

    Preconditions.checkArgument((hooks.length == numStates),
                                "Wrong rowsize for hooks. Expected %s got %s",
                                numStates,
                                hooks.length);

    Preconditions.checkArgument((stateHooks.length == numStates),
                                "Wrong rowsize for stateHooks. Expected %s got"
                                + " %s",
                                numStates,
                                hooks.length);
    Preconditions.checkArgument((stateMachines.length == numStates),
                                "Wrong number of StateMachines specified."
                                + " Expected %s got %s",
                                numStates,
                                stateMachines.length);

    // Check that the entry for each state in both the tables has the
    // correct number of event entries.
    for (int i = 0; i < numStates; i++) {
      Preconditions.checkArgument((transitionTable[i].length == numEvents),
                                  "Incorrect number of event entries in"
                                  + " transitionTable"
                                  + "[%s]. Expected %s got %s",
                                  i,
                                  numEvents,
                                  transitionTable[i].length);

      Preconditions.checkArgument((hooks[i].length == numEvents),
                                  "Incorrect number of event entries in hooks"
                                  + "[%s]. Expected %s got %s",
                                  i,
                                  numEvents,
                                  hooks[i].length);
    }

    // Check that the startState is a valid state.
    if ((startState >= numStates) || (startState < 0)) {
      throw new IllegalArgumentException(
          String.format("Invalid start state %d, valid values are 0 to %d",
                        startState,
                        numStates - 1));
    }

    // Check that all transitions are into valid states.
    for (int i = 0; i < numStates; i++) {
      for (int j = 0; j < numEvents; j++) {
        int entry = transitionTable[i][j];
        if ((entry >= 0) && (entry >= numStates)) {
          throw new IllegalArgumentException(
              String.format("Invalid entry in transitionTable[%d][%d], got %d"
                            + "while valid range is %d to %d",
                            i,
                            j,
                            entry,
                            -1,
                            numStates - 1));
        }
      }
    }

    this.eventIndex = eventIndex;
    this.transitionTable = transitionTable;
    this.hooks = hooks;
    this.stateHooks = stateHooks;
    this.states = states;
    this.stateMachines = stateMachines;
    this.name = name;

    // Set the currentState to the startState.
    currentState = startState;
  }

  /**
   * Returns the index of the given event; -1 if the event does not
   * exist in the index.
   */
  private int getEventIndex(EventType eventType) {
    if (!eventIndex.containsKey(eventType)) {
      return -1;
    } else {
      return eventIndex.get(eventType);
    }
  }

  /**
   * Returns the state machine associated with the state. This does a
   * simple indexing into the stateMachines array, it is up to the
   * creator of the instance of the GenericStateMachine put the
   * appropriate values in the stateMachies array which is passed in
   * the constructor.
   */
  State getStateMachine(int index) {
    return stateMachines[index];
  }


  /**
   * Returns true if the statemachine handles the given event. To be
   * more precise it will return if the current state will handle the
   * given event.
   */
  public boolean handles(EventType e) {
    /**
     * An event is handled if either this state machine handles it or
     * it is handled by a recursive state machine.
     */
    return (thisHandles(e) || recursiveHandles(e));
  }

  /**
   * Returns true if currentState is a recursive state and the
   * recursive state machine handles the given event.
   */
  private boolean recursiveHandles(EventType e) {
    State recursiveState = getStateMachine(currentState);
    if (recursiveState != null) {
      return recursiveState.handles(e);
    } else {
      return false;
    }
  }

  /**
   * Returns true if this state machine (not recursive ones) has a
   * transition defined for the currentState and given event.
   */
  private boolean thisHandles(EventType e) {
    int eindex = getEventIndex(e);
    if (eindex == -1) {
      return false;
    }
    if (transitionTable[currentState][eindex] >= 0) {
      return true;
    } else {
      return false;
    }
  }

  public void handleEvent(final Event event) {
    // See if the {@code currentState} handles this event.
    if (!handles(event.getType())) {
      throw new NotImplementedStateException(
          String.format("Unhandled Event %s in State %s of  %s",
                        event,
                        states[currentState],
                        this));
    }
    final int eventIndex = getEventIndex(event.getType());
    if (eventIndex >= 0) {
      // Run all specified hooks.
      List<EventCallback> callbackList = hooks[currentState][eventIndex];
      if (callbackList != null) {
        for (EventCallback callback : callbackList) {
          try {
            callback.run(event);
          } catch (Exception e) {
            logger.log(Level.WARNING,String.format(
                       "Event callback for state %s, event %s in %s "
                       + "threw exception %s",
                       states[currentState],
                       event,
                       this,
                       e.toString()));
          }
        }
      }
    }

    /**
     * If this state machine has a transition defined for the
     * combination of the currentState and Event then make the
     * transition otherwise delegate it to the recursive state. This
     * ensures that transitions defined at the parent state machine
     * take precedence over transitons defined in recursive State
     * Machines. Also we know that if this state machine doesn't
     * define a transition for the current state and event, then the
     * recursive state does handle the event (since the handles method
     * has returned true).
     */
    if (thisHandles(event.getType())) {
      final int nextState = transitionTable[currentState][eventIndex];
      currentState = transitionTable[currentState][eventIndex];

      // If there are any State hooks attached to the state we have
      // transitioned into then run those.
      List<StateCallback> stateCallbacks = stateHooks[currentState];
      if (stateCallbacks != null) {
        for (StateCallback callback : stateCallbacks) {
          try {
            callback.stateEntered(getCurrentStateName());
          } catch (Exception e) {
            logger.log(Level.WARNING, String.format(
                       "state callback for state {0}, in {1} threw exception",
                       states[currentState],
                       name));
          }
        }
      }
    } else {
      State recursiveState = getStateMachine(currentState);
      recursiveState.handleEvent(event);
    }
  }

  @Override
  public String toString() {
    return name;
  }

  /**
   * Returns a textual description of the State Machine.
   */
  public String printStateMachine() {
    StringBuilder output = new StringBuilder();
    // Print the States
    output.append("States \n\n");
    for (int i = 0; i < states.length; i++) {
      output.append(String.format("%s = %d \n", states[i], i));
    }

    // Print the Events
    output.append("----------------------------------------------\n");
    output.append("Events \n\n");
    for (EventType eventType : eventIndex.keySet()) {
      output.append(
          String.format("%s = %d \n", eventType, getEventIndex(eventType)));
    }
    return output.toString();
  }

  /**
   * Returns the current state the machine is in. To be used primarily
   * for testing.
   */
  int getCurrentState() {
    return currentState;
  }

  /**
   * Returns the name of the current state.
   */
  public String getCurrentStateName() {
    return states[currentState];
  }

  /**
   * Sets the current state of the machine. To be used primarily for
   * testing. Throws {@code IllegalArgumentException} if an invalid state
   * entry is given.
   */
  void setCurrentState(int state) {
    if ((state >= numStates) || (state < 0)) {
      throw new IllegalArgumentException(
          String.format("Invalid state %d, valid values are 0 to %d",
                        state,
                        numStates - 1));
    }
    currentState = state;
  }
}
